﻿using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;

namespace Narwhunderful.Aphelia.Framework.IO.Serialization.Binary
{
    /// <summary>

    public sealed class BinaryPropertyDeserializer : IPropertyDeserializer
    {
        private readonly IBinaryReader _reader;
        private IDictionary<int, ComplexProperty> complexItems = new Dictionary<int, ComplexProperty>();

        public BinaryPropertyDeserializer(IBinaryReader reader)
        {
            if (reader == null) throw new ArgumentNullException("reader");
            _reader = reader;
        }

        public void Open(Stream stream)
        {
            _reader.Open(stream);
        }

        public Property Deserialize()
        {
            byte elementId = _reader.ReadElementId();
            return Deserialize(elementId, null);
        }

        public void Close()
        {
            _reader.Close();
        }

        private Property Deserialize(byte elementId, Type expectedType)
        {
            // Estimate property name
            string propertyName = _reader.ReadName();
            return Deserialize(elementId, propertyName, expectedType);
        }

        private Property Deserialize(byte elementId, string propertyName, Type expectedType)
        {
            // Estimate property type
            Type propertyType = _reader.ReadType();

            // id propertyType is not defined, we'll take the expectedType
            if (propertyType == null)
            {
                propertyType = expectedType;
            }

            // create the property
            Property property = CreateProperty(elementId, propertyName, propertyType);
            if (property == null) return null;

            // Null property?
            var nullProperty = property as NullProperty;
            if (nullProperty != null)
            {
                return nullProperty;
            }

            // is it simple property?
            var simpleProperty = property as SimpleProperty;
            if (simpleProperty != null)
            {
                ParseSimpleProperty(simpleProperty);
                return simpleProperty;
            }

            var multiDimensionalArrayProperty = property as MultiDimensionalArrayProperty;
            if (multiDimensionalArrayProperty != null)
            {
                ParseMultiDimensionalArrayProperty(multiDimensionalArrayProperty);
                return multiDimensionalArrayProperty;
            }

            var singleDimensionalArrayProperty = property as SingleDimensionalArrayProperty;
            if (singleDimensionalArrayProperty != null)
            {
                ParseSingleDimensionalArrayProperty(singleDimensionalArrayProperty);
                return singleDimensionalArrayProperty;
            }

            var dictionaryProperty = property as DictionaryProperty;
            if (dictionaryProperty != null)
            {
                ParseDictionaryProperty(dictionaryProperty);
                return dictionaryProperty;
            }

            var collectionProperty = property as CollectionProperty;
            if (collectionProperty != null)
            {
                ParseCollectionProperty(collectionProperty);
                return collectionProperty;
            }

            var complexProperty = property as ComplexProperty;
            if (complexProperty != null)
            {
                ParseComplexProperty(complexProperty, elementId == Elements.ComplexObjectWithId);
                return complexProperty;
            }

            var complexReferenceProperty = property as ComplexReferenceProperty;
            if (complexReferenceProperty != null)
            {
                ParseComplexReferenceProperty(complexReferenceProperty);
                return complexReferenceProperty;
            }

            return property;
        }

        private void ParseComplexReferenceProperty(ComplexReferenceProperty property)
        {
            int complexReferenceId = _reader.ReadNumber();

            ComplexProperty target;
            if (complexItems.TryGetValue(complexReferenceId, out target))
                property.ReferenceTarget = target;
            else
            {
                string message = string.Format("{0}-parser : Cannot find <{6} {4}='{5}'/>  when resolving <{1} {2} ='{3}' {4}='{5}'/>",
                    GetType().Name,
                    Narwhunderful.Aphelia.Framework.IO.Serialization.Xml.Elements.ComplexObjectReference,
                    Narwhunderful.Aphelia.Framework.IO.Serialization.Xml.Attributes.Name, property.Name,
                    Narwhunderful.Aphelia.Framework.IO.Serialization.Xml.Attributes.ComplexReferenceId, complexReferenceId,
                    Narwhunderful.Aphelia.Framework.IO.Serialization.Xml.Elements.ComplexObject);
                throw new FormatException(message);
            }

        }

        private void ParseComplexProperty(ComplexProperty property, bool withReferenceId)
        {
            if (withReferenceId)
            {
                property.ComplexReferenceId = _reader.ReadNumber();
                complexItems.Add(property.ComplexReferenceId, property);
            }

            // There are properties
            ReadProperties(property.Properties, property.Type);
        }

        private void ReadProperties(PropertyCollection properties, Type ownerType)
        {
            int count = _reader.ReadNumber();
            for (int i = 0; i < count; i++)
            {
                byte elementId = _reader.ReadElementId();

                string propertyName = _reader.ReadName();

                // estimating the propertyInfo
                PropertyInfo subPropertyInfo = ownerType.GetProperty(propertyName);

                Property subProperty = Deserialize(elementId, propertyName, subPropertyInfo.PropertyType);
                properties.Add(subProperty);
            }
        }

        private void ParseCollectionProperty(CollectionProperty property)
        {
            // Element type
            property.ElementType = _reader.ReadType();

            // Properties
            ReadProperties(property.Properties, property.Type);

            // Items
            ReadItems(property.Items, property.ElementType);
        }

        private void ParseDictionaryProperty(DictionaryProperty property)
        {
            // expected key type
            property.KeyType = _reader.ReadType();

            // expected value type
            property.ValueType = _reader.ReadType();

            // Properties
            ReadProperties(property.Properties, property.Type);

            // Items
            ReadDictionaryItems(property.Items, property.KeyType, property.ValueType);
        }

        private void ReadDictionaryItems(IList<KeyValueItem> items, Type expectedKeyType, Type expectedValueType)
        {
            // count
            int count = _reader.ReadNumber();

            // items
            for (int i = 0; i < count; i++)
            {
                ReadDictionaryItem(items, expectedKeyType, expectedValueType);
            }
        }

        private void ReadDictionaryItem(IList<KeyValueItem> items, Type expectedKeyType, Type expectedValueType)
        {
            // key
            byte elementId = _reader.ReadElementId();
            Property keyProperty = Deserialize(elementId, expectedKeyType);

            // value
            elementId = _reader.ReadElementId();
            Property valueProperty = Deserialize(elementId, expectedValueType);

            // add the item
            var item = new KeyValueItem(keyProperty, valueProperty);
            items.Add(item);
        }

        private void ParseSingleDimensionalArrayProperty(SingleDimensionalArrayProperty property)
        {
            // Element type
            property.ElementType = _reader.ReadType();

            // Lowerbound
            property.LowerBound = _reader.ReadNumber();

            ReadItems(property.Items, property.ElementType);
        }

        private void ReadItems(ICollection<Property> items, Type expectedElementType)
        {
            int count = _reader.ReadNumber();
            for (int i = 0; i < count; i++)
            {
                byte elementId = _reader.ReadElementId();
                Property subProperty = Deserialize(elementId, expectedElementType);
                items.Add(subProperty);
            }
        }

        private void ParseMultiDimensionalArrayProperty(MultiDimensionalArrayProperty property)
        {
            // Element Type
            property.ElementType = _reader.ReadType();

            // Dimension Infos
            ReadDimensionInfos(property.DimensionInfos);

            // Items
            ReadMultiDimensionalArrayItems(property.Items, property.ElementType);
        }

        private void ReadMultiDimensionalArrayItems(IList<MultiDimensionalArrayItem> items, Type expectedElementType)
        {
            // count
            int count = _reader.ReadNumber();

            // items
            for (int i = 0; i < count; i++)
            {
                ReadMultiDimensionalArrayItem(items, expectedElementType);
            }
        }

        private void ReadMultiDimensionalArrayItem(IList<MultiDimensionalArrayItem> items, Type expectedElementType)
        {
            // Coordinates
            int[] indexes = _reader.ReadNumbers();

            // item itself
            byte elementId = _reader.ReadElementId();
            Property value = Deserialize(elementId, expectedElementType);
            var item = new MultiDimensionalArrayItem(indexes, value);
            items.Add(item);
        }

        private void ReadDimensionInfos(IList<DimensionInfo> dimensionInfos)
        {
            // count
            int count = _reader.ReadNumber();

            // Dimensions
            for (int i = 0; i < count; i++)
            {
                ReadDimensionInfo(dimensionInfos);
            }
        }

        private void ReadDimensionInfo(IList<DimensionInfo> dimensionInfos)
        {
            var info = new DimensionInfo();
            info.Length = _reader.ReadNumber();
            info.LowerBound = _reader.ReadNumber();

            dimensionInfos.Add(info);
        }

        private void ParseSimpleProperty(SimpleProperty property)
        {
            // There is value
            property.Value = _reader.ReadValue(property.Type);
        }

        private Property CreateProperty(byte elementId, string propertyName, Type propertyType)
        {
            switch (elementId)
            {
                case Elements.SimpleObject:
                    return new SimpleProperty(propertyName, propertyType);
                case Elements.ComplexObject:
                case Elements.ComplexObjectWithId:
                    return new ComplexProperty(propertyName, propertyType);
                case Elements.ComplexObjectReference:
                    return new ComplexReferenceProperty(propertyName);
                case Elements.Collection:
                    return new CollectionProperty(propertyName, propertyType);
                case Elements.Dictionary:
                    return new DictionaryProperty(propertyName, propertyType);
                case Elements.SingleArray:
                    return new SingleDimensionalArrayProperty(propertyName, propertyType);
                case Elements.MultiArray:
                    return new MultiDimensionalArrayProperty(propertyName, propertyType);
                case Elements.Null:
                    return new NullProperty(propertyName);
                default:
                    return null;
            }
        }
    }
}
